<?php

declare(strict_types=1);

namespace Core;

use InvalidArgumentException;
use RuntimeException;

/**
 * 一个实现了模板继承功能的简单视图渲染类。
 *
 * 支持 `{extend}` 和 `{block}` 标签，允许创建可重用的布局。
 * 该类被设计为 final，并通过静态方法调用，不应被继承或实例化。
 */
final class View
{
    /**
     * 模板文件的根目录路径。
     * 默认路径设置为 __DIR__ . '/../app/view'
     * @var string
     */
    private static string $basePath; // 移除 ?，使其不能为空

    /**
     * 模板文件扩展名。
     * @var string
     */
    private static string $extension = '.php';

    /**
     * 存储已解析的区块内容的数组。
     * @var array<string, string>
     */
    private static array $blocks = [];

    // 在类定义中直接设置默认 basePath
    // 注意：静态属性的默认值不能是表达式（如 __DIR__ . '...'）
    // 因此，我们在类加载后、首次使用前通过一个静态初始化方法来设置它。
    // 一种常见的做法是在第一个需要它的静态方法中检查并设置。
    // 或者，可以使用一个专门的初始化块（虽然不常见于PHP）。
    // 这里采用在 resolvePath 中检查并设置默认值的方式。

    /**
     * 禁止实例化。这是一个纯静态工具类。
     */
    private function __construct()
    {
    }

    /**
     * 初始化视图组件，设置模板根目录。
     *
     * @param string $path 视图文件的根目录绝对路径。
     * @return void
     */
    public static function initialize(string $path): void
    {
        if (!is_dir($path)) {
            throw new InvalidArgumentException("View path does not exist: {$path}");
        }
        self::$basePath = rtrim($path, '/\\');
    }

    /**
     * 确保 $basePath 已经设置。如果未设置，则使用默认路径。
     * 这个辅助方法确保在首次调用 resolvePath 时设置默认值。
     * @return void
     */
    private static function ensureBasePathInitialized(): void
    {
        if (!isset(self::$basePath) || self::$basePath === '') {
             // 设置默认路径
             self::$basePath = rtrim(__DIR__ . '/../app/view', '/\\');
             // 再次验证默认路径是否存在（可选，但推荐）
             if (!is_dir(self::$basePath)) {
                 throw new RuntimeException("默认视图路径不存在: " . self::$basePath);
             }
        }
    }

    /**
     * 渲染一个视图文件。
     *
     * @param string $viewName 视图文件的名称 (例如 'index' 或 'products/list')。
     * @param array<string, mixed> $data 传递给视图的变量。
     * @return void
     */
    public static function render(string $viewName, array $data = []): void
    {
        // 确保 basePath 已初始化
        self::ensureBasePathInitialized();

        $viewFile = self::resolvePath($viewName);

        // 读取模板文件内容
        $content = file_get_contents($viewFile);

        if ($content === false) {
             throw new RuntimeException("Failed to read view file: {$viewFile}");
        }

        // 解析并处理 {extend} 标签
        $parentTemplateContent = self::parseExtend($content);

        // 解析 {block} 标签 (来自子模板)
        self::parseBlocks($content);

        // 如果存在父模板，则使用父模板内容；否则使用当前模板内容
        $finalContent = $parentTemplateContent ?? $content;

        // 将父模板中的 {block} 替换为子模板中定义的内容
        $renderedContent = self::replaceBlocks($finalContent);

        // 渲染最终的 PHP 模板
        self::renderPhpContent($renderedContent, $data);
    }

    /**
     * 解析 {extend} 标签，获取并返回父模板内容。
     *
     * @param string &$content 模板内容的引用，{extend} 标签会被移除。
     * @return string|null 如果找到父模板，返回其内容；否则返回 null。
     */
    private static function parseExtend(string &$content): ?string
    {
        $pattern = '/\{extend\s+name="([^"]+)"\s*\/?\}/';
        if (preg_match($pattern, $content, $matches)) {
            $parentViewName = $matches[1];
            // 从原内容中移除 extend 标签
            $content = preg_replace($pattern, '', $content, 1);
            $parentViewFile = self::resolvePath($parentViewName);
            $parentContent = file_get_contents($parentViewFile);
            if ($parentContent === false) {
                 throw new RuntimeException("Failed to read parent view file: {$parentViewFile}");
            }
            return $parentContent;
        }
        return null;
    }

    /**
     * 解析模板中的所有 {block} 标签，并将其内容存入 self::$blocks。
     *
     * @param string $content 模板内容。
     * @return void
     */
    private static function parseBlocks(string $content): void
    {
        // 使用 PREG_OFFSET_CAPTURE 来获取匹配的偏移量，以便正确处理嵌套
        $pattern = '/\{block\s+name="(\w+)"\}(.*?)\{\/block\}/s';
        if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                // $match[1] 是区块名, $match[2] 是区块内容
                self::$blocks[$match[1]] = $match[2];
            }
        }
    }

    /**
     * 将模板中的 {block} 标签替换为已解析的区块内容。
     *
     * @param string $content 待处理的模板内容（通常是父模板）。
     * @return string 替换完成后的内容。
     */
    private static function replaceBlocks(string $content): string
    {
        return preg_replace_callback(
            '/\{block\s+name="(\w+)"\}(.*?)\{\/block\}/s',
            function ($matches) {
                $blockName = $matches[1];
                $defaultContent = $matches[2];
                // 如果在子模板中定义了该区块，则使用子模板的内容，否则使用父模板的默认内容
                return self::$blocks[$blockName] ?? $defaultContent;
            },
            $content
        );
    }

    /**
     * 将 PHP 变量注入模板并执行渲染。
     * 使用 include 和临时文件或流包装器来替代 eval。
     *
     * @param string $content 包含 PHP 代码的最终模板字符串。
     * @param array<string, mixed> $data 要注入的变量。
     * @return void
     */
    private static function renderPhpContent(string $content, array $data): void
    {
        // 将数组键名作为变量名，键值作为变量值
        extract($data, EXTR_SKIP);

        // 清空区块缓存，为下一次渲染做准备
        self::$blocks = [];

        // --- 使用临时文件替代 eval ---
        // 创建一个唯一的临时文件名
        $tempFileName = tempnam(sys_get_temp_dir(), 'view_');

        if ($tempFileName === false) {
            throw new RuntimeException('Failed to create temporary file for view rendering.');
        }

        // 将处理后的内容写入临时文件
        if (file_put_contents($tempFileName, $content) === false) {
             unlink($tempFileName); // 尝试清理
             throw new RuntimeException('Failed to write content to temporary file.');
        }

        // 开启输出缓冲
        ob_start();

        // 包含临时文件，这会执行其中的 PHP 代码
        // 错误会被正常的 PHP 错误处理机制捕获
        include $tempFileName;

        // 获取并清除输出缓冲区内容
        $output = ob_get_clean();

        // 删除临时文件
        unlink($tempFileName);

        // 输出渲染后的内容
        echo $output;
    }


    /**
     * 根据视图名称解析为完整的文件路径。
     *
     * @param string $viewName 视图名称。
     * @return string 完整的文件路径。
     * @throws RuntimeException 如果视图文件不存在或基路径无效。
     */
    private static function resolvePath(string $viewName): string
    {
         // 确保 basePath 已初始化 (双重保险)
         self::ensureBasePathInitialized();

         // 检查 basePath 是否有效 (虽然是在 initialize 或 ensureBasePathInitialized 中设置的，但再检查一次更安全)
         if (empty(self::$basePath) || !is_dir(self::$basePath)) {
              throw new RuntimeException('View base path is invalid or not set correctly.');
         }

        $file = self::$basePath . DIRECTORY_SEPARATOR . str_replace(['.', '/'], DIRECTORY_SEPARATOR, $viewName) . self::$extension;
        if (!file_exists($file)) {
            throw new RuntimeException("View file not found: {$file}");
        }
        return $file;
    }
}
?>